package com.bpmn.aop;

import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.bpmn.common.annotation.ApiSignature;
import com.bpmn.common.exception.ServiceException;
import com.bpmn.common.utils.ServletUtils;
import com.bpmn.system.domain.SysTenant;
import com.bpmn.system.service.ISysTenantService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.Objects;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 拦截声明了 {@link ApiSignature} 注解的方法，实现签名
 *
 * @author rain
 */
@Aspect
@Slf4j
@AllArgsConstructor
@Component
public class ApiSignatureAspect {

    private final ISysTenantService tenantService;

    // 使用ConcurrentHashMap作为本地缓存，用于存储已使用的nonce，键为nonce值，值为过期时间戳（单位：秒）
    private static final ConcurrentHashMap<String, Long> NONCE_CACHE = new ConcurrentHashMap<>();

    @Before("@annotation(signature)")
    public void beforePointCut(JoinPoint joinPoint, ApiSignature signature) {
        // 1. 验证通过，直接结束
        if (verifySignature(signature, Objects.requireNonNull(ServletUtils.getRequest()))) {
            return;
        }
        // 2. 验证不通过，抛出异常
        log.error("[beforePointCut][方法{} 参数({}) 签名失败]", joinPoint.getSignature().toString(),
                joinPoint.getArgs());
        throw new ServiceException(StrUtil.format("[方法{} 参数({}) 签名失败]", joinPoint.getSignature().toString(), joinPoint.getArgs()));
    }

    public boolean verifySignature(ApiSignature signature, HttpServletRequest request) {
        // 1.1 校验 Header
        if (!verifyHeaders(signature, request)) {
            return false;
        }
        // 1.2 校验 appId 是否能获取到对应的 appSecret
        String appId = request.getHeader(signature.appId());
        // 1.3 从数据库中获取 appSecret
        SysTenant sysTenant = tenantService.getOne(new LambdaQueryWrapper<SysTenant>().eq(SysTenant::getAppId, appId));
        String appSecret = sysTenant.getAppSecret();
        Assert.notNull(appSecret, "[appId({})] 找不到对应的 appSecret", appId);

        // 2. 校验签名【重要！】
        // 客户端签名
        String clientSignature = request.getHeader(signature.sign());
        // 服务端签名字符串
        String serverSignatureString = buildSignatureString(signature, request, appSecret);
        // 服务端签名
        String serverSignature = DigestUtil.sha256Hex(serverSignatureString);
        return !ObjUtil.notEqual(clientSignature, serverSignature);
    }

    /**
     * 校验请求头加签参数
     * <p>
     * 1. appId 是否为空
     * 2. timestamp 是否为空，请求是否已经超时，默认 10 分钟
     * 3. nonce 是否为空，随机数是否 10 位以上，是否在规定时间内已经访问过了
     * 4. sign 是否为空
     *
     * @param signature signature
     * @param request   request
     * @return 是否校验 Header 通过
     */
    private boolean verifyHeaders(ApiSignature signature, HttpServletRequest request) {
        // 1. 非空校验
        String appId = request.getHeader(signature.appId());
        if (StrUtil.isBlank(appId)) {
            return false;
        }
        String timestamp = request.getHeader(signature.timestamp());
        if (StrUtil.isBlank(timestamp)) {
            return false;
        }
        String nonce = request.getHeader(signature.nonce());
        if (StrUtil.length(nonce) < 10) {
            return false;
        }
        String sign = request.getHeader(signature.sign());
        if (StrUtil.isBlank(sign)) {
            return false;
        }
        long currentTime = System.currentTimeMillis();
        // 2. 检查 timestamp 是否超出允许的范围 （重点一：此处需要取绝对值）
        long expireTime = signature.timeUnit().toMillis(signature.timeout());
        long requestTimestamp = Long.parseLong(timestamp);
        long timestampDisparity = Math.abs(currentTime - requestTimestamp);
        if (timestampDisparity > expireTime) {
            return false;
        }
        // 3. 首先从缓存中获取对应 nonce 的过期时间戳，如果存在且未过期（即过期时间大于当前时间），则认为该 nonce 已被使用过，返回 false 表示验证失败；
        // 若 nonce 不在缓存中或者已过期，则将其添加到缓存中，并设置新的过期时间（当前时间加上过期时间间隔），然后继续进行签名本身的验证操作
        Long expirationTime = NONCE_CACHE.get(nonce);
        if (expirationTime != null && expirationTime > currentTime) {
            return false;
        }
        // 将当前nonce添加到缓存中，并设置过期时间
        NONCE_CACHE.put(nonce, currentTime + expireTime * 2);
        return true;
    }

    /**
     * 构建签名字符串
     * <p>
     * 格式为 = 请求参数 + 请求体 + 请求头 + 密钥
     *
     * @param signature signature
     * @param request   request
     * @param appSecret appSecret
     * @return 签名字符串
     */
    private String buildSignatureString(ApiSignature signature, HttpServletRequest request, String appSecret) {
        // 请求头
        SortedMap<String, String> parameterMap = getRequestParameterMap(request);
        // 请求参数
        SortedMap<String, String> headerMap = getRequestHeaderMap(signature, request);
        // 请求体
        String requestBody = StrUtil.nullToDefault(ServletUtils.getBody(request), "");
        return MapUtil.join(parameterMap, "&", "=")
                + requestBody
                + MapUtil.join(headerMap, "&", "=")
                + appSecret;
    }

    /**
     * 获取请求头加签参数 Map
     *
     * @param request   请求
     * @param signature 签名注解
     * @return signature params
     */
    private static SortedMap<String, String> getRequestHeaderMap(ApiSignature signature, HttpServletRequest request) {
        SortedMap<String, String> sortedMap = new TreeMap<>();
        sortedMap.put(signature.appId(), request.getHeader(signature.appId()));
        sortedMap.put(signature.timestamp(), request.getHeader(signature.timestamp()));
        sortedMap.put(signature.nonce(), request.getHeader(signature.nonce()));
        return sortedMap;
    }

    /**
     * 获取请求参数 Map
     *
     * @param request 请求
     * @return queryParams
     */
    private static SortedMap<String, String> getRequestParameterMap(HttpServletRequest request) {
        SortedMap<String, String> sortedMap = new TreeMap<>();
        for (Map.Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
            sortedMap.put(entry.getKey(), entry.getValue()[0]);
        }
        return sortedMap;
    }

}